﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace TPOJ.CoreWF
{

    public sealed class RunActivity : CodeActivity<Result>
    {
        private Result result = new Result();
        private string commandLine;
        private bool isExitedEventHandled = false; //Used for threads synchronization.
        private bool isOutputDataHandled = false; //The caller waits for events being handled.
        private CodeActivityContext context;

        public InArgument<string> InputPath { get; set; }
        public InArgument<string> OutputPath { get; set; }
        public InArgument<string> BinPath { get; set; }
        public InArgument<int> TimeLimit { get; set; }
        public InArgument<int> MemoryLimit { get; set; }
        public InArgument<Language> Language { get; set; }

        protected override Result Execute(CodeActivityContext context)
        {
            this.TimeLimit = (context.GetValue(TimeLimit) + context.GetValue(Language).TimeFix) * context.GetValue(Language).TimeFactor;
            this.MemoryLimit = context.GetValue(MemoryLimit) + context.GetValue(Language).MemoryFix;
            this.context = context;
            ParseCommandLine();
            return Run();
        }

        /// <summary>
        /// Load and run the program.
        /// </summary>
        /// <returns>The running result</returns>
        public Result Run()
        {
            result.ResultType = TPOJ.CoreWF.ResultType.RuntimeError;
            Sandbox sandbox = new Sandbox();

            ProcessStartInfo startInfo = new ProcessStartInfo(this.commandLine);
            startInfo.WindowStyle = ProcessWindowStyle.Hidden;
            startInfo.CreateNoWindow = true;
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            if (this.InputPath != null)
                startInfo.RedirectStandardInput = true;

            //Process.EnterDebugMode();
            Process p = sandbox.CreateProcess(startInfo);
            p.EnableRaisingEvents = true;
            p.Exited += p_Exited;
            p.OutputDataReceived += new DataReceivedEventHandler(p_OutputDataReceived);

            StreamWriter input = p.StandardInput;
            StreamReader reader = new StreamReader(context.GetValue(InputPath));
            input.WriteLine(reader.ReadToEnd());

            p.BeginOutputReadLine();

            if (sandbox.WaitForDebugEvent(context.GetValue(TimeLimit)))
            {
                result.ExitCode = 0;
                result.ErrorMessage = null;
            }
            else
            {
                sandbox.Kill();
                if (string.IsNullOrEmpty(sandbox.ErrorMessage))
                {
                    result.ExitCode = -1;
                    result.ResultType = TPOJ.CoreWF.ResultType.TimeLimitExceeded;
                }
                else
                {
                    result.ExitCode = -1;
                    result.ErrorMessage = sandbox.ErrorMessage;
                }
            }
            //Process.LeaveDebugMode();           

            if (string.IsNullOrEmpty(result.ErrorMessage))
            {
                p.Kill();
                //Wait for process exit.
                while (!p.HasExited)
                    Thread.Sleep(100);
                //Wait for process exited event to be handled.
                while (!this.isExitedEventHandled)
                    Thread.Sleep(100);
                //Wait for process output data received event to be handled.
                while (!this.isOutputDataHandled)
                    Thread.Sleep(100);
            }
            else
            {
                p.Exited -= p_Exited;
                p.Kill();
            }
            p.Close();
            p.Dispose();

            FixResult();
            return result;
        }

        /// <summary>
        /// Parse the configuration to get the actual command line.
        /// </summary>
        private void ParseCommandLine()
        {
            this.commandLine = context.GetValue(Language).RunCommandLine.Replace("%1", context.GetValue(BinPath));
        }

        /// <summary>
        /// Fix the result to balance the difference between different runtime performance.
        /// </summary>
        private void FixResult()
        {
            result.Memory -= context.GetValue(Language).MemoryFix;
            result.Time -= context.GetValue(Language).TimeFix;
            result.Time /= context.GetValue(Language).TimeFactor;
            result.Memory = (result.Memory > 0) ? result.Memory : 0;
            result.Time = (result.Time > 0) ? result.Time : 0;
        }

        /// <summary>
        /// User program process exited event handler.
        /// Tests for MemoryLimitExceeded.
        /// </summary>
        /// <param name="sender">User process</param>
        /// <param name="e">EventArgs</param>
        private void p_Exited(object sender, EventArgs e)
        {
            Process process = sender as Process;
            long usedMemory = process.PeakPagedMemorySize64;
            if (usedMemory / 1024 > context.GetValue(MemoryLimit))
            {
                if (string.IsNullOrEmpty(result.ErrorMessage))
                {
                    result.ExitCode = -3;
                    result.ResultType = TPOJ.CoreWF.ResultType.MemoryLimitExceeded;
                }
            }
            result.Time = process.TotalProcessorTime.TotalMilliseconds;
            result.Memory = usedMemory / 1024;
            isExitedEventHandled = true;
        }

        /// <summary>
        /// User program process output data received event handler.
        /// Write output data to output file for comparing.
        /// </summary>
        /// <param name="sender">User process</param>
        /// <param name="e">EventArgs</param>
        private void p_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            using (StreamWriter writer = File.AppendText(context.GetValue(OutputPath)))
            {
                if (e.Data != null)
                    writer.WriteLine(e.Data);
            }
            isOutputDataHandled = true;
        }
    }
}
